/*
 * Copyright 2012 Igor Benicio de Mesquita
 * benizzio@gmail.com
 * 
 * This file is part of fleaNJECT.
 * 
 * fleaNJECT is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * fleaNJECT is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with fleaNJECT.  If not, see <http://www.gnu.org/licenses/>.
 */

package fleanject.di.impl;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import fleanject.di.AbstractConfiguration;
import fleanject.di.Assembler;
import fleanject.di.exception.DIAssemblerException;


public class AnnotationAssembler implements Assembler {
	
	private Map<String, Class<?>> componentClassMap = new HashMap<String, Class<?>>();

	@Override
	public void configure(AbstractConfiguration configuration) {
		
		AnnotationConfiguration annotationConfiguration = (AnnotationConfiguration) configuration;
		
		for(Class<?> configurationClass : annotationConfiguration.getConfigurations()) {
			
			String componentClassIdentifier;
			
			Named namedAnnotation = configurationClass.getAnnotation(Named.class);
			
			if(namedAnnotation != null) {
				componentClassIdentifier = namedAnnotation.value();
			}
			else {
				componentClassIdentifier = configurationClass.getName();
			}
			
			this.componentClassMap.put(componentClassIdentifier, configurationClass);
		}
	}
	
	@Override
	public Object getOrAssembleInto(String name, Map<String, Object> beanMap) {
		
		// Search for a instanced singleton or another instanced bean
		Object bean = beanMap.get(name);
		
		if(bean != null) { return bean; }
		// Prototype or not instanced yet
		else {
			// Get the component class containing annotated configs
			Class<?> componentClass = this.componentClassMap.get(name);
			
			if(componentClass == null) { throw new DIAssemblerException("No configuration or non component bean " +
					"found for \"" + name + "\""); }
			
			// Assemble bean and resolve injections
			Object componentInstance = doAssembleInto(componentClass, beanMap);
			
			// If it's a singleton, map the instance
			if(componentClass.getAnnotation(Singleton.class) != null) {
				beanMap.put(name, componentInstance);
			}
			
			return componentInstance;
		}
	}
	
	private Object doAssembleInto(Class<?> componentClass, Map<String, Object> beanMap) {
		
		// Creates the object instance
		Object componentInstance = createInstance(componentClass);
		
		// Look for @Inject methods
		for(Method method : componentClass.getMethods()) {
			
			if(method.getAnnotation(Inject.class) != null) {
				
				String dependencyKey = this.resolveDependencyKey(method);
				
				// Recursively get or assemble dependency
				Object dependency = getOrAssembleInto(dependencyKey, beanMap);
				
				// Invoke injection method
				try {
					method.invoke(componentInstance, dependency);
				}
				catch(Throwable e) {
					throw new DIAssemblerException("Error injecting dependency \"" + dependencyKey + "\" in " +
							componentClass.getName(), e);
				}
			}
		}
		
		return componentInstance;
	}
	
	private Object createInstance(Class<?> componentClass) {
		
		try {
			return componentClass.newInstance();
		}
		catch(Throwable e) {
			throw new DIAssemblerException("Error creating instance of " + componentClass.getName(), e);
		}
	}
	
	private String resolveDependencyKey(Method injectionMethod) {
		
		// Look for a @Named qualifier
		Named namedMethodAnnotation = injectionMethod.getAnnotation(Named.class);
		if(namedMethodAnnotation != null) {
			return namedMethodAnnotation.value();
		}
		else {
			
			// Look if the parameter class for the injection method is mapped to get its name
			Class<?> injectionParameterClass = injectionMethod.getParameterTypes()[0];
			if(this.componentClassMap.containsKey(injectionParameterClass.getName())) {
				return injectionParameterClass.getName();
			}
			// If not mapped, looks for a assignable bean class to get its name
			else {
				for(Entry<String, Class<?>> componentClassMapEntry : this.componentClassMap.entrySet()) {
					if(injectionParameterClass.isAssignableFrom(componentClassMapEntry.getValue())) {
						return componentClassMapEntry.getKey();
					}
				}
			}
		}
		
		// If the dependency name can't be found, throws exception
		throw new DIAssemblerException("Unable to find correct dependency to inject in \"" + injectionMethod.getName() + 
				"\" (" + injectionMethod.getDeclaringClass().getName() + ")");
	}
}